﻿/*=========================================================================================================================/
 * 
 * FILE: Level.cs
 * AUTH: Matthew Baum
 * DATE: 10/29/2011
 * CHANGE: 11/27/2011
 * 
 * ADDED:
 *          - Scrolling screen along X AND Y
 *          - Minimum orb count required to clear level (75%)
 *          - Player life counter
 *          - Added loading for Drillable tiles & tile destruction function
 *          - Added management of energy cells
 * 
 *=========================================================================================================================
 *
 * BASED ON:
 * 
 *-------------------------------------------------------------------------------------------------------------------------
 * Level.cs
 *
 * Microsoft XNA Community Game Platform
 * Copyright (C) Microsoft Corporation. All rights reserved.
 *-------------------------------------------------------------------------------------------------------------------------*/

// System includes
using System;
using System.Collections.Generic;
using System.IO;

// XNA includes
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Input.Touch;
using Microsoft.Xna.Framework.Input;

namespace Homebound
{
    /// <summary>
    /// A uniform grid of tiles with collections of gems and enemies.
    /// The level owns the player and controls the game's win and lose
    /// conditions as well as scoring.
    /// </summary>
    class Level : IDisposable
    {
        // Physical structure of the level. (2D array of tiles)
        private Tile[,] tiles;

        // Background layers (each layer is a set (array) of textures)
        private Layer[] layers;

        // The layer which entities are drawn on top of. (3 layers of background
        private const int EntityLayer = 2;

        // Entities in the level.
        // The player
        public Player Player
        {
            get { return player; }
        }
        public Player player;

        // All the collectibles
        private List<Orb> gems = new List<Orb>();

        // All the enemies
        public List<Enemy> enemies = new List<Enemy>();
        public int enemyC = 0;

        // Energy Cells to heal life
        private List<EnergyCell> cellList = new List<EnergyCell>();

        // Key locations in the level.
        // Player spawn/start point
        private Vector2 start;

        // Level exit
        private Point exit = InvalidPosition;
        private static readonly Point InvalidPosition = new Point(-1, -1);

        // Level game state.
        private Random random = new Random(354668); // Arbitrary, but constant seed

        // Level state: Camera position: leftmost viewable location
        public float cameraLeft;

        // Level state: Camera position: topmost viewable location
        public float cameraTop;

        // Player score
        public int Score
        {
            get { return score; }
        }
        public int score;

        // Orb count
        public string OrbC
        {
            get { return orbC + "/" + totalOrbC; }
        }
        public int orbC, totalOrbC = 0;

        // Minimum number of orbs that must be collected to clear the level
        private float ORB_LIMIT = 0.75f;

        // Boolean: cleared level
        public bool ReachedExit
        {
            get { return reachedExit; }
        }
        bool reachedExit;

        // Level counter
        public TimeSpan TimeRemaining
        {
            get { return timeRemaining; }
        }
        TimeSpan timeRemaining;

        // Points awarded per second remaining
        private const int PointsPerSecond = 5;

        // Level content.        
        public ContentManager Content
        {
            get { return content; }
        }
        ContentManager content;

        private SoundEffect exitReachedSound;

        // Bachir: Scrolling Text
        public SpriteFont scrollFont;
        public List<ScrollingText> scrollingText;

        #region Loading

        /// <summary>
        /// Constructs a new level.
        /// </summary>
        /// <param name="serviceProvider">
        /// The service provider that will be used to construct a ContentManager.
        /// </param>
        /// <param name="fileStream">
        /// A stream containing the tile data.
        /// </param>
        public Level(IServiceProvider serviceProvider, Stream fileStream, int levelIndex, ContentManager newContent)
        {
            // Create a new content manager to load content used just by this level.
            content = newContent;

            timeRemaining = TimeSpan.FromMinutes(6.0);

            LoadTiles(fileStream);

            // Matthew: Create 3 Layers of depth, each with a different scrolling speed
            // Scroll speed: 0-1 (0: no scroll, 1: as fast as frontmost (tiles) level)
            layers = new Layer[3];
            layers[0] = new Layer(Content, "Backgrounds/space_0", 0.2f);
            layers[1] = new Layer(Content, "Backgrounds/Layer1", 0.5f);
            layers[2] = new Layer(Content, "Backgrounds/Layer2", 0.8f);

            // Load sounds.
            exitReachedSound = Content.Load<SoundEffect>("Sounds/ExitReached");

            // Scrolling Text List Initialization
            scrollFont = Content.Load<SpriteFont>("Fonts/Scroll");
            scrollingText = new List<ScrollingText>();
        }

        /// <summary>
        /// Iterates over every tile in the structure file and loads its
        /// appearance and behavior. This method also validates that the
        /// file is well-formed with a player start point, exit, etc.
        /// </summary>
        /// <param name="fileStream">
        /// A stream containing the tile data.
        /// </param>
        private void LoadTiles(Stream fileStream)
        {
            // Load the level and ensure all of the lines are the same length.
            int width;
            List<string> lines = new List<string>();

            // Read in level file, and make sure the format (shape) is correct
            using (StreamReader reader = new StreamReader(fileStream))
            {
                string line = reader.ReadLine();
                width = line.Length;
                while (line != null)
                {
                    lines.Add(line);
                    if (line.Length != width)
                        throw new Exception(String.Format("The length of line {0} is different from all preceeding lines.", lines.Count));
                    line = reader.ReadLine();
                }
            }

            // Allocate the tile grid.
            tiles = new Tile[width, lines.Count];

            // Loop over every tile position,
            for (int y = 0; y < Height; ++y)
            {
                for (int x = 0; x < Width; ++x)
                {
                    // Load a tile: type represented by char in level file
                    char tileType = lines[y][x];
                    tiles[x, y] = LoadTile(tileType, x, y);
                }
            }

            // Verify that the level has a beginning and an end.
            if (Player == null)
                throw new NotSupportedException("A level must have a starting point.");

            if (exit == InvalidPosition)
                throw new NotSupportedException("A level must have an exit.");

            // Set enemy count
            enemyC = enemies.Count;
        }

        /// <summary>
        /// Loads an individual tile's appearance and behavior.
        /// </summary>
        /// <param name="tileType">
        /// The character loaded from the structure file which
        /// indicates what should be loaded.
        /// </param>
        /// <param name="x">
        /// The X location of this tile in tile space.
        /// </param>
        /// <param name="y">
        /// The Y location of this tile in tile space.
        /// </param>
        /// <returns>The loaded tile.</returns>
        private Tile LoadTile(char tileType, int x, int y)
        {
            switch (tileType)
            {
                // Blank space
                case '.':
                    return new Tile(null, TileCollision.Passable);

                // Exit
                case 'X':
                    return LoadExitTile(x, y);

                // Gem
                case 'G':
                    return LoadOrbTile(x, y);

                // Energy Cells
                case 'E':
                    return LoadCellTile(x, y);

                // Floating platform
                case '-':
                    return LoadTile("RockB0", TileCollision.Platform);

                // Various enemies
                case 'A':
                    return LoadEnemyTile(x, y, "MonsterA");
                case 'B':
                    return LoadEnemyTile(x, y, "Bird");

                //case 'C':
                    //return LoadEnemyTile(x, y, "MonsterC");
                //case 'D':
                    //return LoadEnemyTile(x, y, "MonsterD");

                // Platform block
                //case '~':
                    //return LoadVarietyTile("BlockB", 2, TileCollision.Platform);

                // Passable block
                //case ':':
                    //return LoadVarietyTile("BlockB", 2, TileCollision.Passable);

                // Player 1 start point
                case '1':
                    return LoadStartTile(x, y);

                // Impassable block
                case '#':
                    return LoadVarietyTile("RockA", 2, TileCollision.Impassable);

                // Drillable Tile
                case '>':
                    return LoadTile("RockC0", TileCollision.Drillable);

                // Unknown tile type character
                default:
                    throw new NotSupportedException(String.Format("Unsupported tile type character '{0}' at position {1}, {2}.", tileType, x, y));
            }
        }

        /// <summary>
        /// Creates a new tile. The other tile loading methods typically chain to this
        /// method after performing their special logic.
        /// </summary>
        /// <param name="name">
        /// Path to a tile texture relative to the Content/Tiles directory.
        /// </param>
        /// <param name="collision">
        /// The tile collision type for the new tile.
        /// </param>
        /// <returns>The new tile.</returns>
        private Tile LoadTile(string name, TileCollision collision)
        {
            return new Tile(Content.Load<Texture2D>("Tiles/" + name), collision);
        }


        /// <summary>
        /// Loads a tile with a random appearance.
        /// </summary>
        /// <param name="baseName">
        /// The content name prefix for this group of tile variations. Tile groups are
        /// name LikeThis0.png and LikeThis1.png and LikeThis2.png.
        /// </param>
        /// <param name="variationCount">
        /// The number of variations in this group.
        /// </param>
        private Tile LoadVarietyTile(string baseName, int variationCount, TileCollision collision)
        {
            int index = random.Next(variationCount);
            return LoadTile(baseName + index, collision);
        }


        /// <summary>
        /// Instantiates a player, puts him in the level, and remembers where to put him when he is resurrected.
        /// </summary>
        private Tile LoadStartTile(int x, int y)
        {
            if (Player != null)
                throw new NotSupportedException("A level may only have one starting point.");

            start = RectangleExtensions.GetBottomCenter(GetBounds(x, y));
            player = new Player(this, start);
            player.CanSpeedUp = true;       // TODO Add logic for when the player is allowed to use the speed-up power-up.
            player.CanDoubleJump = true;    // TODO Add logic for when the player is allowed to use the double-jump power-up.

            return new Tile(null, TileCollision.Passable);
        }

        /// <summary>
        /// Remembers the location of the level's exit.
        /// </summary>
        private Tile LoadExitTile(int x, int y)
        {
            if (exit != InvalidPosition)
                throw new NotSupportedException("A level may only have one exit.");

            exit = GetBounds(x, y).Center;

            return LoadTile("Exit", TileCollision.Passable);
        }

        /// <summary>
        /// Instantiates an enemy and puts him in the level.
        /// </summary>
        private Tile LoadEnemyTile(int x, int y, string spriteSet)
        {
            Vector2 position = RectangleExtensions.GetBottomCenter(GetBounds(x, y));
            switch (spriteSet)
            {
                case "Bird": enemies.Add(new Bird(this, position, content));
                    break;

                default: enemies.Add(new Enemy(this, position, spriteSet, content));
                    break;
            }

            return new Tile(null, TileCollision.Passable);
        }

        /// <summary>
        /// Instantiates an orb and puts it in the level.
        /// </summary>
        private Tile LoadOrbTile(int x, int y)
        {
            Point position = GetBounds(x, y).Center;
            gems.Add(new Orb(this, new Vector2(position.X, position.Y)));

            // Increment orbs count
            ++totalOrbC;

            return new Tile(null, TileCollision.Passable);
        }

        // Create a new Energy Cell tile, and add to level
        private Tile LoadCellTile(int x, int y)
        {

            Point position = GetBounds(x, y).Center;
            cellList.Add(new EnergyCell( new Vector2(position.X, position.Y) ) );

            return new Tile(null, TileCollision.Passable);

        }

        /// <summary>
        /// Unloads the level content.
        /// </summary>
        public void Dispose()
        {
            Content.Unload();
        }

        #endregion

        #region Bounds and collision

        /// <summary>
        /// Gets the collision mode of the tile at a particular location.
        /// This method handles tiles outside of the levels boundries by making it
        /// impossible to escape past the left or right edges, but allowing things
        /// to jump beyond the top of the level and fall off the bottom.
        /// </summary>
        public TileCollision GetCollision(int x, int y)
        {
            // Prevent escaping past the level ends.
            if (x < 0 || x >= Width)
                return TileCollision.Impassable;

            // Allow jumping past the level top and falling through the bottom.
            if (y < 0 || y >= Height)
                return TileCollision.Passable;

            return tiles[x, y].Collision;
        }

        // Destroy a tile on collision (ex.: drilling)
        public void DestroyTile(int x, int y)
        {
            tiles[x, y] = new Tile( null, TileCollision.Passable);
        }

        /// <summary>
        /// Gets the bounding rectangle of a tile in world space.
        /// </summary>        
        public Rectangle GetBounds(int x, int y)
        {
            return new Rectangle(x * Tile.Width, y * Tile.Height, Tile.Width, Tile.Height);
        }

        /// <summary>
        /// Width of level measured in tiles.
        /// </summary>
        public int Width
        {
            get { return tiles.GetLength(0); }
        }

        /// <summary>
        /// Height of the level measured in tiles.
        /// </summary>
        public int Height
        {
            get { return tiles.GetLength(1); }
        }

        #endregion

        #region Update

        /// <summary>
        /// Updates all objects in the world, performs collision between them,
        /// and handles the time limit with scoring.
        /// </summary>
        
        public void Update(GameTime gameTime,
            KeyboardState keyboardState,
            GamePadState gamePadState,
            GraphicsDeviceManager graphics)
        {
            // Pause while the player is dead or time is expired.
            if (!Player.IsAlive || TimeRemaining == TimeSpan.Zero)
            {
                // Still want to perform physics on the player.
                Player.ApplyPhysics(gameTime);
            }
            else if (ReachedExit)
            {
                // Animate the time being converted into points.
                int seconds = (int)Math.Round(gameTime.ElapsedGameTime.TotalSeconds * 100.0f);
                seconds = Math.Min(seconds, (int)Math.Ceiling(TimeRemaining.TotalSeconds));
                timeRemaining -= TimeSpan.FromSeconds(seconds);
                score += seconds * PointsPerSecond;
            }
            else
            {
                timeRemaining -= gameTime.ElapsedGameTime;
                Player.Update(gameTime, keyboardState, gamePadState);
                UpdateGems(gameTime);

                // Falling off the bottom of the level kills the player.
                if (Player.BoundingRectangle.Top >= Height * Tile.Height)
                    OnPlayerKilled(null);

                UpdateEnemies(gameTime, graphics);

                // The player has reached the exit if they are standing on the ground and
                // his bounding rectangle contains the center of the exit tile. They can only
                // exit when they have collected all of the gems.
                if (Player.IsAlive &&
                    Player.IsOnGround &&
                    Player.BoundingRectangle.Contains(exit))
                {
                    OnExitReached();
                }

                // Update energy cells
                UpdateCells( gameTime );
            }

            // Clamp the time remaining at zero.
            if (timeRemaining < TimeSpan.Zero)
                timeRemaining = TimeSpan.Zero;

            // Update Scrolling Text
            foreach (ScrollingText st in scrollingText)
            {
                st.Update(gameTime);
            }
            for (int i = 0; i != scrollingText.Count; i++)
            {
                if (scrollingText[i].expired)
                {
                    scrollingText.RemoveAt(i);
                    i--;
                }
            }
        }

        /// <summary>
        /// Animates each gem and checks to allows the player to collect them.
        /// </summary>
        private void UpdateGems(GameTime gameTime)
        {
            for (int i = 0; i < gems.Count; ++i)
            {
                Orb gem = gems[i];

                gem.Update(gameTime);

                if (gem.BoundingCircle.Intersects(Player.BoundingRectangle))
                {
                    // Remove gem
                    gems.RemoveAt(i--);

                    // Run gem collected fucntion
                    OnGemCollected(gem, Player);

                    // Create display string
                    string txt;
                    Color c;

                    // String depends on LIMIT being reached
                    if(orbC <= (int)(ORB_LIMIT * totalOrbC))
                    {
                        txt = ((int)(ORB_LIMIT * totalOrbC - orbC)).ToString();
                        c = Color.LightBlue;
                    }
                    else
                    {
                        txt = ((int)(totalOrbC - orbC)).ToString();
                        c = Color.LightCoral;
                    }

                    // Display text
                    scrollingText.Add(
                        new ScrollingText(
                            scrollFont,
                            txt,
                            gem.Position - new Vector2(cameraLeft, cameraTop),
                            c
                        )
                    );
                }
            }
        }

        // Update Energy Cells
        private void UpdateCells(GameTime gt)
        {

            // Move backward through the list, so as not to corrupt it, when removing
            for (int i = cellList.Count - 1; i != -1; --i )
            {

                // Update the energy cell position
                cellList[i].update(gt);

                // Collision: update player life and remove the Energy cell
                if (cellList[i].BoundingCircle.Intersects(Player.BoundingRectangle))
                {
                    cellList[i].onCollected( player );
                    scrollingText.Add(new ScrollingText(scrollFont, "HP " + EnergyCell.HEAL, cellList[i].position - new Vector2(cameraLeft, cameraTop), Color.Red));
                    cellList.Remove( cellList[i] );
                }

            }

        }

        /// <summary>
        /// Animates each enemy and allow them to kill the player.
        /// </summary>
        private void UpdateEnemies(GameTime gameTime, GraphicsDeviceManager graphics)
        {
            foreach (Enemy enemy in enemies)
            {
                // Only update enemies inside the viewing window
                // Prevents birds from moving while off screen
                if
                (
                    enemy.Position.X > cameraLeft
                    && enemy.Position.X < (cameraLeft + graphics.PreferredBackBufferWidth)
                    && enemy.Position.Y > cameraTop
                    && enemy.Position.Y < (cameraTop + graphics.PreferredBackBufferHeight)
                )
                {

                    if (enemy is Bird)
                    {
                        Bird bird = (Bird)enemy;
                        bird.Update(gameTime);
                    }
                    else
                    {
                        enemy.Update(gameTime);
                    }

                    // Touching an enemy damages the player, if the player is not already hit
                    if (enemy.BoundingRectangle.Intersects(Player.BoundingRectangle) && !player.isHit)
                    {
                        // Deal damage
                        enemy.dealDamage(player);

                        // IF life <= 0: DEAD
                        if (player.life <= 0)
                        {
                            OnPlayerKilled(enemy);
                        }
                    }
                }
            }

            // Delete dead enemies
            for (int i = enemies.Count - 1; i != -1; --i)
            {
                if (enemies[i].dead)
                {
                    // Update player score
                    score += enemies[i].points;

                    // Kill enemy
                    enemies.Remove(enemies[i]);
                }
            }
        }

        /// <summary>
        /// Called when a gem is collected.
        /// </summary>
        /// <param name="gem">The gem that was collected.</param>
        /// <param name="collectedBy">The player who collected this gem.</param>
        private void OnGemCollected(Orb gem, Player collectedBy)
        {
            score += Orb.PointValue;

            // Increase number of collected gems
            ++orbC;
            // Scrolling Text for collected gem


            gem.OnCollected(collectedBy);
        }

        /// <summary>
        /// Called when the player is killed.
        /// </summary>
        /// <param name="killedBy">
        /// The enemy who killed the player. This is null if the player was not killed by an
        /// enemy, such as when a player falls into a hole.
        /// </param>
        private void OnPlayerKilled(Enemy killedBy)
        {
            Player.OnKilled(killedBy);
        }

        /// <summary>
        /// Called when the player reaches the level's exit.
        /// </summary>
        private void OnExitReached()
        {
            // Exit only IF orb count is high enough
            if (((float)orbC / totalOrbC) >= ORB_LIMIT)
            {
                Player.OnReachedExit();
                exitReachedSound.Play();
                reachedExit = true;
            }
        }

        /// <summary>
        /// Restores the player to the starting point to try the level again.
        /// </summary>
        public void StartNewLife()
        {
            //Player.lives--;

            if (Player.lives >= 0) // The player has lives left.
            {
                Player.Reset(start);
            }
        }

        #endregion

        #region Draw

        /// <summary>
        /// Draw everything in the level from background to foreground.
        /// Matthew: modified to accomodate scrolling layers (textures)
        /// Matthew: only draw gems and enemies IF inside the viewing area
        /// </summary>

        public void Draw(GameTime gameTime, SpriteBatch spriteBatch)
        {

            // Draw ALL background layers
            spriteBatch.Begin();

                for (int i = 0; i <= EntityLayer; ++i)
                    layers[i].Draw(spriteBatch, cameraLeft, -cameraTop);

            spriteBatch.End();

            // SCROLL: camera remains at origin, but the layers are translated backward
            ScrollCamera(spriteBatch.GraphicsDevice.Viewport);

            // Translation matrix (scroll X/Y)
            Matrix cameraTransform = Matrix.CreateTranslation(-cameraLeft, -cameraTop, 0.0f);

            // Sort sprites according to depth, with some alpha blending
            spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default,
                              RasterizerState.CullCounterClockwise, null, cameraTransform);

                // Draw Tiles
                DrawTiles(spriteBatch);

                // Calculate the visible range of window
                // LEFT bound
                float left = cameraLeft;

                // RIGHT bound
                float right = left + spriteBatch.GraphicsDevice.Viewport.Width;

                // TOP bound
                float top = cameraTop;

                // BOTTOM bound
                float bottom = cameraTop + spriteBatch.GraphicsDevice.Viewport.Height;
                
                // Draw Orbs
                foreach (Orb orb in gems)
                {
                    // Draw IF visible
                    if (orb.Position.X >= left && orb.Position.X <= right
                        && orb.Position.Y >= top && orb.Position.Y <= bottom)
                    {
                        orb.Draw(gameTime, spriteBatch);
                    }
                }

                // Draw the player
                Player.Draw(gameTime, spriteBatch);

                // Draw enemies
                foreach (Enemy enemy in enemies)
                {
                    // Draw enemy IF visible
                    if (enemy.Position.X >= left && enemy.Position.X <= right
                        && enemy.Position.Y >= top && enemy.Position.Y <= bottom)
                    {
                        enemy.Draw(gameTime, spriteBatch);
                    }
                }

                // Draw Energy Cells
                DrawCells(spriteBatch);

            spriteBatch.End();

            // Draw foreground textures
            spriteBatch.Begin();

                for (int i = EntityLayer + 1; i < layers.Length; ++i)
                    layers[i].Draw(spriteBatch, cameraLeft, -cameraTop);

            spriteBatch.End();

            // Draw Scrolling Text
            spriteBatch.Begin();
            foreach (ScrollingText st in scrollingText)
            {
                st.Draw(spriteBatch);
            }
            spriteBatch.End();
        }

        /// <summary>
        /// Draws each tile in the level.
        /// </summary>
        private void DrawTiles(SpriteBatch spriteBatch)
        {
            // Calculate the visible range of tiles. (cull non-visible)

            // LEFT bound
            int left = (int)Math.Floor(cameraLeft / Tile.Width);

            // RIGHT bound
            int right = left + spriteBatch.GraphicsDevice.Viewport.Width / Tile.Width;
            right = Math.Min(right, Width - 1);

            // TOP bound
            int top = (int)Math.Floor(cameraTop / Tile.Height);

            // BOTTOM bound
            int bottom = (int)cameraTop + spriteBatch.GraphicsDevice.Viewport.Height / Tile.Height;
            bottom = Math.Min(bottom, Height - 1);

            // For each row in Y range
            for (int y = top; y <= bottom; ++y)
            {
                // For each tile in X range
                for (int x = left; x <= right; ++x)
                {
                    // If there is a visible tile in that position (texture != null)
                    Texture2D texture = tiles[x, y].Texture;

                    if (texture != null)
                    {
                        // Draw it in screen space.
                        Vector2 position = new Vector2(x, y) * Tile.Size;
                        spriteBatch.Draw(texture, position, Color.White);
                    }
                }
            }
        }

        // Darw all energy cells within visible area
        private void DrawCells(SpriteBatch spriteBatch)
        {
            // Calculate the visible range of cells. (cull non-visible)

            // LEFT bound
            int left = (int)Math.Floor(cameraLeft / Tile.Width);

            // RIGHT bound
            int right = left + spriteBatch.GraphicsDevice.Viewport.Width / Tile.Width;
            right = Math.Min(right, Width - 1);

            // TOP bound
            int top = (int)Math.Floor(cameraTop / Tile.Height);

            // BOTTOM bound
            int bottom = (int)cameraTop + spriteBatch.GraphicsDevice.Viewport.Height / Tile.Height;
            bottom = Math.Min(bottom, Height - 1);

            foreach (EnergyCell ec in cellList)
            {
                /*if
                (
                    ec.position.X <= right
                    && ec.position.X >= left
                    && ec.position.Y >= top
                    && ec.position.Y <= bottom
                )*/
                {
                    ec.draw(spriteBatch);
                }
            }
        }

        #endregion

        // Matthew: scroll the current viewable area.
        // Scrolling ends at the boundaries of the level

        private void ScrollCamera(Viewport viewport)
        {
            #if ZUNE
                const float ViewMargin = 0.45f;
                const float ViewMarginY = 0.45f;
            #else
                const float ViewMargin = 0.35f;
                const float ViewMarginY = 0.35f;
            #endif

            // Calculate the edges of the screen.
            // Screen width margin for scrolling
            float marginWidth = viewport.Width * ViewMargin;

            // Screen height margin for scrolling
            float marginHeight = viewport.Height * ViewMarginY;

            // LEFT
            float marginLeft = cameraLeft + marginWidth;

            // RIGHT
            float marginRight = cameraLeft + viewport.Width - marginWidth;

            // TOP
            float marginTop = cameraTop + marginHeight;

            // BOTTOM
            float marginBottom = cameraTop + viewport.Height - marginHeight;

            // Calculate how far to scroll when the player is near the edges of the screen.
            float cameraMovement = 0.0f;

            if (Player.Position.X < marginLeft)
                cameraMovement = Player.Position.X - marginLeft;

            else if (Player.Position.X > marginRight)
                cameraMovement = Player.Position.X - marginRight;

            // Matthew : Y-scroll
            float cameraVertical = 0.0f;

            if (Player.Position.Y < marginTop)
                cameraVertical = Player.Position.Y - marginTop;

            else if (Player.Position.Y > marginBottom)
                cameraVertical = Player.Position.Y - marginBottom;

            // Update the camera position, but prevent scrolling off the ends of the level.
            float maxCameraPosition = Tile.Width * Width - viewport.Width;
            float maxCameraY = Tile.Height * Height - viewport.Height;

            cameraLeft = MathHelper.Clamp(cameraLeft + cameraMovement, 0.0f, maxCameraPosition);

            // Matthew: clamp on Y
            cameraTop = MathHelper.Clamp(cameraTop + cameraVertical, 0.0f, maxCameraY);
        }
    }
}
